#Requires -Modules Pester

if (-not (test-path Variable:\pester)) {
    throw "script must be run with Invoke-Pester.ps1"
}

Set-StrictMode -Version 3
$ErrorActionPreference = "Stop"

$here = $PSScriptRoot
$scriptDir = join-path (Split-Path -Parent $PSScriptRoot) DevBoard

$pkgDir = "%PKGDIR%"

function MakeTestFiles {
    param (
        [parameter(Mandatory=$true)]
        $root, 

        [parameter(ValueFromPipeline=$true)]
        [string[]]
        $path,

        $itemType
        )

    process {
        foreach ($p in $path) {
            $fullPath = (Join-Path -Path $root -ChildPath $p)
            New-Item -Path $fullPath -ItemType $( 
                if ($itemType -eq $null) {if ($fullPath -match "\.[^\\]+$") {"File"}
                                          else {"Directory"} } 
                else {$itemType}
                ) -Force > $null
            $fullPath
        }
    }
}

function CreateTestBsp {
    param (
        [parameter(Mandatory=$true)]
        $Root, 
        [array] $Infs
        )

    if (Test-Path $Root) {
        Remove-Item -Path $root -Force -Recurse
    }
    New-Item -Path $root -ItemType Directory > $null
    $Infs | MakeTestFiles -root $root -itemType File
}

function GetToolElements {
    param ($doc, [ValidateSet("Firmware", "WindowsTool", "UefiTool")] $type)
    $doc.FeatureManifest.Features.OEM.GetEnumerator() |
        Where-Object {
            [System.Xml.XmlElement] $e = $_
            switch ($type) {
                "Firmware"     {$e.LocalName -eq "FirmwarePackage"; break}
                "WindowsTool"  {$e.LocalName -eq "ToolPackage" -and 
                                $e.GetAttribute("Type") -eq "Windows"; break}
                "UefiTool"     {$e.LocalName -eq "ToolPackage" -and 
                                $e.GetAttribute("Type") -eq "Uefi"; break}
            }
        }
}

Describe "BSP" {
    # workaround for an issue using Pester at strict level 2
    New-Item -Path TestDrive:\placeholder -ItemType File -Force > $null

    Import-Module $scriptDir\DevBoard.psm1 -DisableNameChecking -Force -ArgumentList @("TestMode")

    Context "New-BspManifest" {
    
        $src = "TestDrive:\New-BspManifest"
        $infs = CreateTestBsp -Root $src `
                              -Infs @("testinf1\testinf1.inf"
                                      "testinf2\testinf2.inf"
                                      "testinf3\testinf3.inf")
        
        $output = Join-Path $src "manifest.xml"
    
        $bsp = New-BspManifest -RootDirectory $src
    
        It "creates an empty BSP object" {
            $bsp | Should Not Be $null
        }
    
        It "returns an empty manifest file" {
            {$bsp.XmlDocument} | Should Not Throw
            $doc = $bsp.XmlDocument
            $doc | Should Not Be $null
            $doc.FeatureManifest.BasePackages | Should Not Be $null
            $doc.FeatureManifest.Features.OEM | Should Not Be $null
        }
    
        It "can save the empty document" {
            {$bsp.Save($output)} | Should Not Throw
            $output | Should Exist
        }
        
        It "saves a valid manifest" {
            {$doc = loadAndValidateManifest $output} | Should Not Throw
        }
        
        It "sets the default namespace at the document element" {
            # check that the root element has the default namespace specified
            $bsp.XmlDocument.DocumentElement.NamespaceURI | Should Be "DevboardBspManifestSchema.xsd"
        }
        
        It "Verifies that the Root directory exists" {
            {New-BspManifest -RootDirectory TestDrive:\does_not_exist} | 
                Should Throw "`$RootDirectory does not exist, or is not a directory"
        }
        
        It "Stores the root with a trailing \ in all cases" {
            $paths = @("TestDrive:\New-BspManifest", "TestDrive:\New-BspManifest\") 
        
            $paths | 
                ForEach-Object {(New-BspManifest -RootDirectory $_).Root.Path} |
                Should Match "\\$"
        }
        
        It "Sets the BSP root to an absolute path if input is relative" {
            Push-Location TestDrive:\
            $bsp = New-BspManifest -RootDirectory "New-BspManifest"
            $bsp2 = New-BspManifest -RootDirectory ".\New-BspManifest"
            Pop-Location
        
            (Split-Path -IsAbsolute $bsp.Root) | Should Be $true
            (Split-Path -IsAbsolute $bsp2.Root) | Should Be $true
            
            $bsp.Root.Path | Should Match "\\$"
            $bsp2.Root.Path | Should Match "\\$"
        }
    }
    
    Context "Add Drivers" {
    
        $src = "TestDrive:\BSP Drivers"
        $infs = CreateTestBsp -Root $src `
                              -Infs @("testinf1\testinf1.inf"
                                      "testinf2\testinf2.inf"
                                      "testinf3\testinf3.inf"
                                      "foo\bar\baz.inf"
                                      "this\that\other.inf",
                                      "duplicate\path\other.inf")
        
        $output = Join-Path $src "manifest.xml"
        
        It "adds a driver given a full path under Root" {
            $bsp = New-BspManifest -RootDirectory $src
            $path = join-path $src "foo\bar\baz.inf"
            {
                $bsp.AddContent("Driver", "$path") 
            } | Should not Throw
            $bsp.Drivers["baz.inf"] | Should Be "foo\bar\baz.inf"
        }
    
        It "fails to add a driver given a full path not under Root" {
            $path = "TestDrive:\not_the_root\baz.inf"
            $bsp = New-BspManifest -RootDirectory $src
            { $bsp.AddContent("Driver", $path) } | Should Throw "Absolute `$ItemPath must be under Root"
        }
    
        It "adds a driver given a relative path as if it was under Root" {
            $path = "this\that\other.inf"
            $bsp = New-BspManifest -RootDirectory $src
            { $bsp.AddContent("Driver", $path) } | Should Not Throw
            $bsp.Drivers["other.inf"] | Should Be "$path"
        }
    
        It "strips . out of relative paths" {
            Push-Location $src
            try {
                $bsp = New-BspManifest -RootDirectory .
    
                $bsp.AddContent("Driver", ".\testinf1\testinf1.inf")
    
                $bsp.Drivers["testinf1.inf"] | Should Be "testinf1\testinf1.inf"
    
            } finally {
                Pop-Location
            }
        }
    
        It "emits a warning when replacing a driver with the same name" {
            $bsp = New-BspManifest -RootDirectory $src
            $bsp.AddContent("Driver", "this\that\other.inf")
            {$bsp.AddContent("Driver", "duplicate\path\other.inf") 3> TestDrive:\warning} | Should Not Throw 
            (get-content "TestDrive:\warning" -Raw).StartsWith("Overwriting item other.inf entry [") | Should Be $true
            $bsp.Drivers["other.inf"] | Should Be "duplicate\path\other.inf"
        }
    
        It "can add multiple drivers" {
            $bsp = New-BspManifest -RootDirectory $src
            [array] $testInfs = @($infs | Where-Object {$_ -like "testinf*"})
            for ($i = 0; $i -lt $testInfs.Length; $i += 1) {
                $bsp.AddContent("Driver", $testInfs[$i])
                $bsp.Drivers.Count | Should Be $i + 1
            }
        }
    }
    
    Context "Remove Items" {
        $src = "TestDrive:\New-BspManifest"
        $infs = CreateTestBsp -Root $src `
                              -Infs @("testinf1\testinf1.inf"
                                      "testinf2\testinf2.inf"
                                      "testinf3\testinf3.inf")
        
        $output = Join-Path $src "manifest.xml"
    
        $bsp = New-BspManifest -RootDirectory $src
        $infs | ForEach-Object {$bsp.AddContent("Driver", $_)}
        
        It "allows removal of drivers using absolute paths" {
            {$bsp.RemoveContent($infs[0])} | Should Not Throw
            $bsp.Drivers.Keys -contains "testinf1.inf" | Should Be $false
        }
    
        It "allows removal of drivers using relative paths" {
            {$bsp.RemoveContent("testinf2\testinf2.inf")} | Should Not Throw
            $bsp.Drivers.Keys -contains "testinf2.inf" | Should Be $false
        }
    
        It "prints warning when driver to remove is not in the BSP" {
            {
            $bsp.RemoveContent("testinf2\testinf2.inf") 3> TestDrive:\warnings
            } | Should Not Throw
            (get-content TestDrive:\warnings) | Should Be "Driver testinf2.inf not found"
        }
    }
    
    Context "Saving BSP manifest" {
    
        $src = "TestDrive:\Saving BSP Manifest"
        $infs = CreateTestBsp -Root $src `
                              -Infs @("testinf1\testinf1.inf"
                                      "testinf2\testinf2.inf"
                                      "testinf3\testinf3.inf")
        
        $output = Join-Path $src "manifest.xml"
        $bsp = New-BspManifest -RootDirectory $src
        $infs | ForEach-Object {$bsp.AddContent("Driver", $_)}
            
        It "adds drivers to the manifest as well-formed XML" {
            { $bsp.Save($output) } | Should Not Throw
            { loadAndValidateManifest $output } | Should Not Throw
        }

        It "adds each INF to the base packages with the correct path" {
            $doc = $bsp.XmlDocument
            $pf = $doc.FeatureManifest.BasePackages.PackageFile
        
            $pf.Length | Should Be $infs.Count
        
            foreach ($i in $infs) {
        
                $matchCount = 0
                
                $name = Split-Path -Path $i -Leaf
                $relativePath = ([string]$i).Replace($src, "")
    
                $entries = @($pf | Where-Object -Property Name -EQ -Value $name)
                
                $entries.Length | Should Be 1
                $entries[0].Path | Should Be (Split-Path -Parent (Join-Path -Path $pkgDir -ChildPath $relativePath))
            }
        }
    }

    Context "With -Import set" {
        $src = "TestDrive:\Importing BSP"
        $infs = CreateTestBsp -Root $src `
                              -Infs @("testinf1\testinf1.inf"
                                      "testinf2\testinf2.inf"
                                      "testinf3\testinf3.inf")
    
        # add one file (not part of the INF list we validate against) which is in the 
        # ignore file
        New-Item -Path "$src\testinf4\testinf4.inf" -ItemType File -Value "testinf4.inf" -Force > $null
        $ignore = New-Item -Path "$src\.ignore" -ItemType File
        "\testinf4\testinf4.inf"  > $ignore
        "\testinf5\testinf5.inf" >> $ignore
    
        $output = Join-Path $src "manifest.xml"
        
        It "Scans the root directory for INFs" {
            {New-BspManifest -RootDirectory $src -Import 3> $null } | Should Not Throw
        }
    
        $bsp = New-BspManifest -RootDirectory $src -Import 3> "$src\warnings"
        $ignore = Get-Content "$src\.ignore"
    
        It "Adds the INFs to the internal hashtable" {
            $bsp.Drivers.Keys.Count | Should Be $infs.Count
            $bsp.Drivers["testinf2.inf"] | Should Be "testinf2\testinf2.inf"
        }
    
        It "Ignores INFs found in the .ignore file" {
            $bsp.Drivers.Keys -contains "testinf4.inf" | Should Be $false
            $warnings = @(Get-Content "$src\warnings")
            $warnings.Length | Should Be 1
            $warnings -like "Ignore file entry*testinf4*" | should be $null
            $warnings -like "Ignore file entry*testinf5*" | should not be $null
        }
    
        It "can save the doc according to the schema" {
            { $bsp.Save($output) } | Should Not Throw
            {loadAndValidateManifest $output}  | Should Not Throw
        }
        
        $doc = [xml] (Get-Content -Path $output)
        $xmlmgr = getNamespaceManager $doc
    
        It "Imports all the INFs in the directory tree" {
            # check INFs
            $doc.FeatureManifest.BasePackages.PackageFile.Count | Should Be $infs.Count
            
            foreach ($inf in $infs) {
                $infName = Split-Path -Leaf $inf
                $node = $doc.SelectSingleNode(
                            "bsp:FeatureManifest/bsp:BasePackages/bsp:PackageFile[@Name='$infName']", 
                            $xmlmgr
                            ) 
                $node | Should Not Be $null
            }
        }
    
        It "doesn't do anything with tools or firmware" {
            $nodes = @(
                $doc.SelectNodes("//bsp:OEM/*", $xmlmgr)
                )
            $nodes.Length | Should Be 0
        }
    }
    
    Context "Adding tools" {
        $src = "TestDrive:\AddingTools"
        $infs = CreateTestBsp -Root $src `
                              -Infs @("testinf1\testinf1.inf"
                                      "testinf2\testinf2.inf"
                                      "testinf3\testinf3.inf")

        # add a directory with some tools in it
        $tools = MakeTestFiles -root $src `
                               -path @("winToolDirectory",
                                       "foo.ps1",
                                       "firmware\updatefirmware.efi",
                                       "firmware\RandomTool.efi")

        $toolTypes = @("WindowsTool", "WindowsTool", "Firmware", "UefiTool")
        
        # POPULATE the wintools directory
        (1 .. 10) | 
            ForEach-Object {"winToolDirectory\File$_.stuff"} | 
            MakeTestFiles -root $src | 
            Out-Null
    
        $bsp = New-BspManifest -RootDirectory $src

        It "supports adding tools of all types" {

            for ($i = 0; $i -lt $toolTypes.Count; $i += 1) {
                $toolType = $tooltypes[$i]
                $toolDir = $tools[$i]
                
                $testItemDir = Split-Path  $toolDir -Parent
                $testItemName = Split-Path $toolDir -Leaf

                { 
                    $bsp.AddContent($toolType, $toolDir) 
                } | Should Not Throw

                $bsp.Drivers.ContainsKey($testItemName) | Should Be $false
                $bsp.Items.ContainsKey($testItemName)   | Should Be $true

                $item = $bsp.Items[$testItemName]

                $item.Type | Should Be $toolType
                $item.Name | Should Be $testItemName
                $item.Path | Should Match ([RegEx]::Escape($item.Path) + "$")
            }
        }

        $doc = $bsp.XmlDocument
        

        It "adds Windows tools into the XML manifest" {
            (GetToolElements -doc $doc -type WindowsTool | Measure-Object).Count | Should Be 2
        }

        It "adds UEFI tools into the XML manifest" {
           (GetToolElements -doc $doc -type UefiTool | Measure-Object).Count | Should Be 1
        }

        It "adds Firmware into the XML manifest" {
           (GetToolElements -doc $doc -type Firmware | Measure-Object).Count | Should Be 1
        }
    }

    # 
    # Context "importing a BSP from a manifest file" {
    # 
    # }
}
